/*
* FLINT PARTICLE SYSTEM
* .....................
* 
* Author: Richard Lord
* Copyright (c) Richard Lord 2008-2011
* http://flintparticles.org
* 
* 
* Licence Agreement
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package org.flintparticles.threed.actions;

import nme.geom.Vector3D;
import org.flintparticles.common.particles.Particle;
import org.flintparticles.common.actions.ActionBase;
import org.flintparticles.common.activities.FrameUpdatable;
import org.flintparticles.common.activities.UpdateOnFrame;
import org.flintparticles.common.emitters.Emitter;
import org.flintparticles.common.events.ParticleEvent;
import org.flintparticles.threed.emitters.Emitter3D;
import org.flintparticles.threed.particles.Particle3D;

/**
 * The Collide action detects collisions between particles and modifies their velocities
 * in response to the collision. All paticles are approximated to a circular shape for
 * the collisions and they are assumed to be off even density.
 * 
 * <p>If the particles reach a stationary, or near stationary, state under an accelerating 
 * force (e.g. gravity) then they will fall through each other. This is due to the nature
 * of the alogorithm used, which is designed for speed of execution and sufficient accuracy 
 * when the particles are in motion, not for absolute precision.</p>
 * 
 * <p>This action has a priority of -20, so that it executes 
 * after other actions.</p>
 */

class Collide extends ActionBase, implements FrameUpdatable
{
	public var bounce(bounceGetter, bounceSetter):Float;
	
	private var _bounce:Float;
	private var _maxDistance:Float;
	private var _updateActivity:UpdateOnFrame;
	
	// used to alternate the direction of parsing the collisions
	private var _sign:Int;
	
	/*
	 * Temporary variables created as class members to avoid creating new objects all the time
	 */
	private var d:Vector3D;

	/**
	 * The constructor creates a Collide action for use by  an emitter.
	 * To add a Collide to all particles created by an emitter, use the
	 * emitter's addAction method.
	 * 
	 * @see org.flintparticles.common.emitters.Emitter#addAction()
	 * 
	 * @param bounce The coefficient of restitution when the particles collide. A value of 
	 * 1 gives a pure elastic collision, with no energy loss. A value
	 * between 0 and 1 causes the particle to loose enegy in the collision. A value greater 
	 * than 1 causes the particle to gain energy in the collision.
	 */
	public function new( bounce:Float= 1 )
	{
		super();
		_sign = 1;
		priority = -20;
		_maxDistance = 0;
		d = new Vector3D();
		this.bounce = bounce;
	}
	
	/**
	 * The coefficient of restitution when the particles collide. A value of 
	 * 1 gives a pure elastic collision, with no energy loss. A value
	 * between 0 and 1 causes the particle to loose enegy in the collision. A value greater 
	 * than 1 causes the particle to gain energy in the collision.
	 */
	private function bounceGetter():Float
	{
		return _bounce;
	}
	private function bounceSetter( value:Float ):Float
	{
		_bounce = value;
		return _bounce;
	}

	/**
	 * Instructs the emitter to produce a sorted particle array for optimizing
	 * the calculations in the update method of this action and
	 * adds an UpdateOnFrame activity to the emitter to call this objects
	 * frameUpdate method once per frame.
	 * 
	 * @param emitter The emitter this action has been added to.
	 * 
	 * @see frameUpdate()
	 * @see org.flintparticles.common.activities.UpdateOnFrame
	 * @see org.flintparticles.common.actions.Action#addedToEmitter()
	 */
	override public function addedToEmitter( emitter:Emitter ) : Void
	{
		cast( emitter, Emitter3D ).spaceSort = true;
		_updateActivity = new UpdateOnFrame( this );
		emitter.addActivity( _updateActivity );
	}

	/**
	 * Removes the UpdateOnFrame activity that was added to the emitter in the
	 * addedToEmitter method.
	 * 
	 * @param emitter The emitter this action has been added to.
	 * 
	 * @see addedToEmitter()
	 * @see org.flintparticles.common.activities.UpdateOnFrame
	 * @see org.flintparticles.common.actions.Action#removedFromEmitter()
	 */
	override public function removedFromEmitter( emitter:Emitter ):Void
	{
		if( _updateActivity != null )
		{
			emitter.removeActivity( _updateActivity );
		}
	}
	
	/**
	 * Called every frame before the particles are updated, this method
	 * calculates the collision radius of the largest two particles, which
	 * aids in optimizing the collision calculations.
	 * 
	 * <p>This method is called using an UpdateOnFrame activity that is
	 * created in the addedToEmitter method.</p>
	 * 
	 * @param emitter The emitter that is using this action.
	 * @param time The duration of the current animation frame.
	 * 
	 * @see org.flintparticles.common.activities.UpdateOnFrame
	 */
	public function frameUpdate( emitter:Emitter, time:Float ):Void
	{
		var particles:Array<Dynamic> = emitter.particlesArray;
		var max1:Float = 0;
		var max2:Float = 0;
		for( p in particles )
		{
			if( p.collisionRadius > max1 )
			{
				max2 = max1;
				max1 = p.collisionRadius;
			}
			else if( p.collisionRadius > max2 )
			{
				max2 = p.collisionRadius;
			}
		}
		_maxDistance = max1 + max2;
		_sign = - _sign;
	}
	
	/**
	 * @inheritDoc
	 */
	override public function update( emitter:Emitter, particle:Particle, time:Float ):Void
	{
		var p:Particle3D = cast( particle, Particle3D );
		var e:Emitter3D = cast( emitter, Emitter3D );
		var particles:Array<Dynamic> = e.particlesArray;
		var other:Particle3D;
		var i:Int;
		var len:Int = particles.length;
		var factor:Float;
		var distanceSq:Float;
		var collisionDist:Float;
		var n1:Float, n2:Float;
		var relN:Float;
		var m1:Float, m2:Float;
		var f1:Float, f2:Float;
		//for( i = p.sortID + _sign; i < len && i >= 0; i += _sign )
		var i = p.sortID + _sign;
		while( i < len && i >= 0 )
		{
			other = particles[i];
			if( ( d.x = other.position.x - p.position.x ) * _sign > _maxDistance ) break;
			collisionDist = other.collisionRadius + p.collisionRadius;
			if( d.x * _sign > collisionDist ) continue;
			d.y = other.position.y - p.position.y;
			if( d.y > collisionDist || d.y < -collisionDist ) continue;
			d.z = other.position.z - p.position.z;
			if( d.z > collisionDist || d.z < -collisionDist ) continue;
			distanceSq = d.lengthSquared;
			if( distanceSq <= collisionDist * collisionDist && distanceSq > 0 )
			{
				d.normalize();
				n1 = p.velocity.dotProduct( d );
				n2 = other.velocity.dotProduct( d );
				relN = n1 - n2;
				if( relN > 0 ) // colliding, not separating
				{
					m1 = p.mass;
					m2 = other.mass;
					factor = ( ( 1 + _bounce ) * relN ) / ( m1 + m2 );
					f1 = factor * m2;
					f2 = -factor * m1;
					
					p.velocity.x -= d.x * f1;
					p.velocity.y -= d.y * f1;
					p.velocity.z -= d.z * f1;
					
					other.velocity.x -= d.x * f2;
					other.velocity.y -= d.y * f2;
					other.velocity.z -= d.z * f2;
					
					if ( emitter.hasEventListener( ParticleEvent.PARTICLES_COLLISION ) )
					{
						var ev:ParticleEvent = new ParticleEvent( ParticleEvent.PARTICLES_COLLISION, p );
						ev.otherObject = other;
						emitter.dispatchEvent( ev );
					}
				}
			}
			i += _sign;
		}
	}
}
